import { Injectable } from '@nestjs/common'; // 导入 NestJS 的 Injectable 装饰器，用于定义依赖注入的服务
import { NodeSSH } from 'node-ssh'; // 导入 node-ssh 库，用于实现 SSH 连接和命令执行
import { Duplex } from 'stream'; // 引入 Duplex 类型

interface SSHSession {
  ssh: NodeSSH;
  currentDirectory: string;
  homeDirectory: string;
  shellStream?: Duplex;
}

@Injectable()
export class RemoteTerminalService {


  /**
   * 定义一个私有属性 sshSessions，它是一个 Map 类型的集合，用于存储 SSH 会话信息。
   * Map 的键是一个字符串，代表客户端的唯一标识符。
   * Map 的值是一个对象，包含以下属性：
   * ssh: NodeSSH 类型，表示一个 SSH 连接实例，用于执行 SSH 命令。
   * currentDirectory: 字符串类型，表示当前工作目录的路径。
   * homeDirectory: 字符串类型，表示用户的家目录路径。
   * shellStream: 可选的 Duplex 流类型，表示一个双向流，用于与 SSH 会话进行交互。
   * @private
   */
  private sshSessions: Map<string, SSHSession> = new Map<string, SSHSession>();


  /**
   * 定义一个私有属性 algorithms，它是一个对象类型，用于配置 SSH 连接的算法。
   * 需要了解服务器支持哪些密钥交换算法。这可以通过使用 SSH 命令行工具（如 ssh）与 -Q kex 选项来查看，或者联系你的服务器管理员获取这些信息。
   * @private
   */
  private algorithms = {
    kex: [
      'ecdh-sha2-nistp256',
      'ecdh-sha2-nistp384',
      'ecdh-sha2-nistp521',
      'diffie-hellman-group-exchange-sha256',
      'diffie-hellman-group14-sha1',
      'diffie-hellman-group1-sha1', // 这是一个较旧的算法，安全性较低，最后尝试
    ],
    cipher: [
      'aes128-ctr',
      'aes192-ctr',
      'aes256-ctr',
      'aes128-gcm@openssh.com',
      'aes256-gcm@openssh.com',
      'aes128-cbc',
      'aes192-cbc',
      'aes256-cbc', // CBC 模式的算法安全性较低，建议谨慎使用
    ],
    hmac: [
      'hmac-sha2-256',
      'hmac-sha2-512',
      'hmac-sha1', // SHA1 的 HMAC 也是较旧的选择
    ],
  };

  // 定义一个设备类型的字段，用于区分不同类型的设备, 默认类型为 Linux
  private deviceType = 'linux';

  /**
   * 初始化会话
   * 检查客户端链接会话是否存在, 存在则返回, 不存在则初始化, 避免重复初始化
   * @param clientId 客户端标识符
   */
  initializeSession(clientId: string) {
    try {
      // 检查是否已存在会话，避免重复初始化
      if (this.sshSessions.has(clientId)) {
        console.log(`会话已存在: ${clientId}`);
        return;
      }

      // 创建新的会话状态
      // ssh: NodeSSH 类型，表示一个 SSH 连接实例，用于执行 SSH 命令
      // currentDirectory: 字符串类型，表示当前工作目录的路径
      // homeDirectory: 字符串类型，表示用户的home目录路径
      // shellStream: 可选的 Duplex 流类型，表示一个双向流，用于与 SSH 会话进行交互
      this.sshSessions.set(clientId, {
        ssh: new NodeSSH(),
        currentDirectory: '/',
        homeDirectory: '/',
        shellStream: undefined,
      });
      console.log(`会话初始化完成: ${clientId}`);
    } catch (error) {
      console.log('初始化会话时发生错误:', error);
    }
  }

  /**
   * 定义一个异步方法 startSSHSession，用于启动一个 SSH 会话
   * @param host 主机地址
   * @param username 用户名
   * @param password 密码
   * @param clientId 客户端标识符
   * @param type 接收设备类型参数
   * @returns {Promise<string>} 返回一个字符串, 代表会话启动成功
   */
  async startSSHSession(host: string, username: string, password: string, clientId: string, type: string ): Promise<string> {
    // 设置设备类型
    this.deviceType = type;
    // 检查会话是否已经初始化
    const session: SSHSession = this.sshSessions.get(clientId);
    if (!session) {
      // 断开连接
      await this.disconnect(clientId);
      return '会话未初始化, 请先初始化会话';
    }
    try {

      // 连接到 SSH 服务器
      // host: string 类型，表示 SSH 服务器的地址
      // username: string 类型，表示 SSH 服务器的用户名
      // password: string 类型，表示 SSH 服务器的密码
      // port: number 类型，表示 SSH 服务器的端口号
      // algorithms: object 类型，表示要使用的算法
      await session.ssh.connect({ host, username, password, port: 22, algorithms: this.algorithms });

      // 请求一个 shell 流，可以指定终端类型。终端类型决定了终端的行为和功能，比如字符编码、颜色支持等。
      // 常见的终端类型包括 'xterm', 'vt100', 'ansi' 等。
      // 'xterm' 是最常用的终端类型，支持颜色和鼠标事件。
      // 'vt100' 是较旧的终端类型，功能较为基础。
      // 'ansi' 也是一种常见的终端类型，支持 ANSI 转义序列。
      session.shellStream = await session.ssh.requestShell({ term: 'xterm' });
      // 更新会话信息
      // session.shellStream = shellStream; // shell 流

      // 如果是 Linux 终端
      if (this.deviceType == 'linux') {
        // 执行命令获取用户的家目录，并去除两端的空白字符
        const homeDirectory = (
          await session.ssh.execCommand('echo $HOME')
        ).stdout.trim();
        session.currentDirectory = homeDirectory; // 当前目录设置为家目录
        session.homeDirectory = homeDirectory; // 家目录
      } else {
        // 如果是路由器或交换机，执行其他初始化设置
        // 例如：session.currentDirectory = '/';
      }

      // 返回一个字符串，表示 SSH 会话已经启动
      return `SSH 会话成功启动, 主机: ${host}, 用户: ${username}`;
    } catch (error) {
      // 如果设备类型是路由器交换机，发送退出命令
      if (this.deviceType === 'device') {
        await this.sendExitCommands(clientId);
      } else {
        await this.disconnect(clientId);
      }
      return 'SSH 会话启动失败,失败原因：' + error.message;
    }
  }

  /**
   * 根据客户端标识符获取对应的 SSH 会话的 shell 流。
   * 如果会话不存在或 shell 流未初始化，则返回 undefined。
   * @param clientId 客户端标识符
   * @returns 返回对应的 Duplex 流，如果会话不存在或 shell 流未初始化则返回 undefined。
   */
  getShellStream(clientId: string): Duplex | undefined {
    try {
      const session: SSHSession = this.sshSessions.get(clientId);
      if (!session) {
        console.error(`未找到客户端ID为 ${clientId} 的会话`);
        return undefined;
      }
      if (!session.shellStream) {
        console.error(`客户端ID为 ${clientId} 的会话中未初始化 shell 流`);
        return undefined;
      }
      return session.shellStream;
    } catch (error) {
      console.log('获取 shell 流时发生错误:', error);
      return undefined;
    }
  }

  /**
   * 发送退出命令到设备, 用于关闭会话
   * 如果会话不存在或 SSH 连接已关闭，则清理会话并返回错误信息
   * @param clientId 客户端标识符
   */
  async sendExitCommands(clientId: string): Promise<string> {
    const session: SSHSession = this.sshSessions.get(clientId);
    if (!session) {
      return '会话不存在';
    }
    if (!session.ssh.isConnected()) {
      return 'SSH连接已关闭';
    }
    if (session.shellStream && session.shellStream.writable) {
      try {
        // 监听错误事件
        session.shellStream.on('error', (error) => {
          console.error('Shell流错误:', error);
          this.cleanupSession(clientId);
        });
        // 监听关闭事件
        session.shellStream.on('close', () => {
          console.log('Shell 流已关闭');
          // 移除所有监听器
          session.shellStream.removeAllListeners();
          this.cleanupSession(clientId);
        });

        session.shellStream.on('data', (data) => {
          console.log('从路由器接收到的数据:', data.toString());
        });
        console.log('-----发送退出命令-----');
        // 发送退出命令
        // await session.ssh.execCommand('\x1A'); // Ctrl+Z
        // await session.ssh.execCommand('quit');
        session.shellStream.write('\x1A');
        // 等待一段时间以确保命令被处理，执行quit命令会导致Shell流关闭，从而触发 close 事件
        session.shellStream.write('quit\n');
        // 确保命令发送完成
        await new Promise((resolve) => setTimeout(resolve, 500));
        session.shellStream.end(); // 关闭写入流
        console.log('-----退出命令已发送-----');
        return '退出命令已发送';
      } catch (error) {
        console.error('设备执行退出命令时发生错误:', error);
        return '设备执行退出命令时发生错误';
      }
    } else {
      // 如果Shell流不可写或不存在，则清理会话
      await this.cleanupSession(clientId);
      return 'Shell流不可写或不存在';
    }
  }

  /**
   * 清理会话和资源, 用于关闭会话, 只释放ssh连接，不释放shell流
   * @param clientId
   */
  async cleanupSession(clientId: string): Promise<void> {
    const session: SSHSession = this.sshSessions.get(clientId);
    if (session && session.ssh.isConnected()) {
      try {
        session.ssh.dispose();
      } catch (error) {
        console.error('释放SSH连接时发生错误:', error);
      }
    }
    this.sshSessions.delete(clientId);
    console.log('cleanupSession SSH会话和资源已成功释放');
  }

  /**
   * 释放ssh连接和shell流并从映射中删除会话
   * @param clientId 客户端标识符
   */
  async disconnect(clientId: string): Promise<string> {
    console.log(`设备断开: ${clientId}`);
    const session: SSHSession = this.sshSessions.get(clientId);
    if (!session) {
      return '会话不存在';
    }
    try {
      // 关闭 shell 流并清除监听器
      if (session.shellStream) {
        //监听流结束事件
        session.shellStream.end();
        session.shellStream.removeAllListeners();
      }
      // 释放 SSH 连接
      if (session.ssh.isConnected()) {
        try {
          session.ssh.dispose();
        } catch (disposeError) {
          console.error('释放 SSH 连接时发生错误:', disposeError);
        }
      }
      // 从映射中删除会话
      this.sshSessions.delete(clientId);
      console.log('disconnect SSH会话和资源已成功释放');
      return 'SSH会话和资源已成功释放';
    } catch (error) {
      console.error('断开连接时发生错误:', error);
      return '断开连接时发生错误';
    }
  }
}
